package interfaz2;

import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;

import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPSecretKey;
import org.eclipse.swt.SWT;
import org.eclipse.swt.browser.Browser;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.FileDialog;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.MenuItem;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeItem;

import core.Blog;
import core.KeyUtils;
import core.Post;
import dustconf.DustConf;
import dustdispatcher.DispatchableTask;
import dustdispatcher.DustDispatcher;
import dustlog.LogController;
import events.UpdateCompleteEvent;
import events.UpdateCompleteListener;

/**
 * The controller of a Blog (MVC design pattern). It is a bit tricky it would
 * be much nicer if I could extend TreeItem, but it cannot be done so finally
 * BlogController has 3 main attributes:
 * 	- blog: The model, a Blog instance.
 * 	- view: The view, a BlogView instance.
 * 	- item: A TreeItem that will be the entry point to the controller. This
 * 			item is used outside in a tree and when it receive events it
 * 			informs to the BlogController attached to it. Every widget in the
 * 			SWT library can have data attached to it, every TreeItem related to
 * 			a Blog has attached to its data its BlogController.
 * 
 * @author ole
 */
public class BlogController {	
	// Class attributes.
	private Blog blog;
	private TreeItem item;
	private BlogView view;
	private Browser browser4posts;
	private MultiViewComposite comp4posts;
	
	private class AddPublicKeyListener extends SelectionAdapter {
		@Override
		public void widgetSelected(SelectionEvent event) {
			String pkey_path;
			FileDialog fdialog;
//			PublicKey key;
			PGPPublicKey key;
			
			// Pop up the dialog and take the path selected.
			fdialog = new FileDialog(new Shell());
			pkey_path = fdialog.open();
			
			// Checks that the users didn't canceled the dialog. If so read
			// the publisher and add it to the blog.
			if (pkey_path != null) {
				try {
					key = KeyUtils.recoverPublicKey(pkey_path);
					blog.addPublicKey(key, pkey_path);
					
					// Log the action.
					LogController.log("Added key to blog " + blog.getTitle());
				}
				catch (Exception e) {
					ExceptionBox ebox;
					ebox = new ExceptionBox(e, new Shell());
					ebox.popError();
				}
			}
		}
	}
	
	/**
	 * Class that will control the selection of the "Add new source" inside the
	 * menu of each Blog.
	 * 
	 * @author ole
	 */
	private class AddNewSourceListener extends SelectionAdapter {	
		@Override
		public void widgetSelected(SelectionEvent event) {
			String url_str;
			NewSourceDialog dialog;
			
			// Pop a dialog to take the new source, check that the dialog
			// returns something and add the new URL to the blog.
			dialog = new NewSourceDialog(new Shell());
			url_str = (String)dialog.runDialog();
			if (url_str != null && !url_str.isEmpty()) {
				try {
					blog.addUrl(new URL(url_str));
					
					// Log the action.
					LogController.log("Added source " + url_str +
									  " to blog " + blog.getTitle());
				}
				catch (Exception e) {
					ExceptionBox ebox;
					ebox = new ExceptionBox(e, new Shell());
					ebox.popError();
				}
			}
		}
	}
	
	/**
	 * Class that will handle the selection of the "Delete blog" option in the
	 * menu that pops up when right click is made over a blog.
	 */
	private class DeleteBlogListener extends SelectionAdapter {
		/**
		 * Delete the blog.
		 */
		@Override
		public void widgetSelected(SelectionEvent event) {
			// Delete the blog from the tree.
			item.dispose();
			
			// Delete the blog from the directory.
			blog.searchAndDelete(DustConf.getSerializedBlogsPath());
			
			// Delete the view of this blog.
			view.dispose();
			
			// Log the action.
			LogController.log("Deleted blog " + blog.getTitle());
		}
	}
	
	/**
	 * Class that handles the selection of the "Publish blog" option in the
	 * menu that pops up when right click is made over a blog.
	 * 
	 * @author ole
	 */
	private class PublishBlogListener extends SelectionAdapter {
		/*
		 * Private classes.
		 */
		
		private class PublishThread extends Thread {
			private PGPSecretKey secr_key;
			private PGPPublicKey pub_key;
			private String pass;
			
			public PublishThread(PGPSecretKey secr_key, PGPPublicKey pub_key,
								 String pass) {
				this.secr_key = secr_key;
				this.pub_key = pub_key;
				this.pass = pass;
			}
			
			@Override
			public void run() {		
				// Log the action.
				LogController.log("Starting to publish blog " +
								  blog.getTitle() + ".");
						
				// Publish the RSS and its images (all dusterized) inside
				// the shared folder.
				try {
					blog.publishBlog(DustConf.getSharedFolder(),
									 secr_key, pub_key, pass);
					
					// Log the action.
					LogController.log("Finished to publish blog " +
							  		  blog.getTitle() + ".");
				}
				catch (Exception e) {
					RunExceptionBox rebox = new RunExceptionBox(e);
					item.getDisplay().syncExec(rebox);
				}
			}
		}
		
		/*
		 * Public methods of class PublishBlogListener.
		 */
		
		@Override
		public void widgetSelected(SelectionEvent event) {
			PublishThread pthread;
			FileDialog fdialog;
			String secr_key_path, pub_key_path, pass;
			PGPSecretKey secr_key;
			PGPPublicKey pub_key;
			AskPassDialog pass_dialog;
			
			// Pop a dialog to get the path to the private key that will be
			// used to sign the RSS and the images.
			fdialog = new FileDialog(getShell(), SWT.NONE);
			fdialog.setText("Select the file that contains the private key for sign.");
			secr_key_path = fdialog.open();
			
			// Pop a dialog to get the password needed to get access to the
			// private key.
			pass_dialog = new AskPassDialog(getShell());
			pass = (String)pass_dialog.runDialog();
			
			// Another dialog to take the public key that will be used to
			// generate the name of the dusterized feed.
			fdialog = new FileDialog(getShell(), SWT.NONE);
			fdialog.setText("Select the file that contains the public key for naming");
			pub_key_path = fdialog.open();
			
			/** TODO: Two dialogs are quite ugly... **/
			
			// Check that the user doesn't canceled it.
			if (secr_key_path != null && pub_key_path != null) {
				try {
					// Recover the keys.
					secr_key = KeyUtils.recoverSecretKey(secr_key_path);
					pub_key = KeyUtils.recoverPublicKey(pub_key_path);
					
					// Start the task that will make the work. This will cause
					// the UI to not get frozen.
					pthread = new PublishThread(secr_key, pub_key, pass);
					pthread.start();
				}
				catch (Exception e) {
					ExceptionBox ebox;
					ebox = new ExceptionBox(e, new Shell());
					ebox.popError();
				}
			}
		}
	}
	
	/**
	 * Class that implements interface Runnable in order to make possible for
	 * the whole threads we manage to pop up dialogs with the errors ocurred.
	 * 
	 * This class is intended to be used as follows:
	 * 	- Create an instance of it.
	 * 	- call getDisplay().asynExec(instance).
	 * 
	 * @author ole
	 */
	private class RunExceptionBox implements Runnable {
		Exception e;
		
		public RunExceptionBox(Exception e) {
			this.e = e;
		}
		
		@Override
		public void run() {
			ExceptionBox ebox;
			ebox = new ExceptionBox(e, new Shell());
			ebox.popError();
		}
	}
	
	/**
	 * Class that will control the selection of the "Update blog" inside the
	 * menu that pops up when right click is made over a blog.
	 * 
	 * @author ole
	 */
	private class UpdateBlogSelectedListener extends SelectionAdapter {
		/*
		 * Private methods and classes.
		 */
		
		/**
		 * Handlers for when a task finished to update a blog. This could be
		 * quite obscure if someone tries to understand this. Let me try to
		 * explain it.
		 * 
		 * First of all, all of this is done because we don't want the GUI to
		 * get frozen while updating a Blog.
		 * 
		 * This is done using Runnables and the asyncExec() method of the
		 * SWT.widget.Display class. When the user decides to update a Blog a
		 * new task (UpdateTask) is created and started, independently from
		 * the UI thread. This will cause that the UI doesn't get frozen during
		 * an update. When we finally update the Blog we need to draw the new
		 * posts in the tree, here comes the Runnable part. When the update()
		 * method of Blog is called it is passed a list of callbacks to be
		 * executed when an update is completed. We create a new type of
		 * UpdateCompleteListener that also implements Runnable, the reason is
		 * that we will need access to UI resources (the Tree), since this
		 * work will be done by other threads instead the UI thread we need
		 * to do it calling asyncExec() in order to make this work in the
		 * UI thread, if we don't we will get an "Invalid Thread Access". This
		 * class implements Runnable to make it possible call it with
		 * asyncExec() and also implements UpdateCompleteListener because we
		 * will need it to be passed to the update() method of the Blog.
		 * 
		 * @author ole
		 */
		private class UIUpdateCompleteListener
		implements UpdateCompleteListener, Runnable {
			private List<UpdateCompleteEvent> events = null;
			
			/**
			 * Class constructor.
			 */
			public UIUpdateCompleteListener() {
				events = new ArrayList<UpdateCompleteEvent>();
			}
			
			/**
			 * When a blog has been updated we need to show the new posts in
			 * its tree.
			 */
			@Override
			public synchronized void handleEvent(UpdateCompleteEvent event) {
				events.add(event);
				item.getDisplay().asyncExec(this);
			}
			
			@Override
			public synchronized void run() {
				UpdateCompleteEvent event;
				
				// Get the next Event to handle.
				event = events.remove(0);
				
				// For each new Post, create its PostController. This will add
				// the node to the Tree and create its PostView.
				for (Post post: event.getPostsAdded())
					
					// Check if a Post with the same title already exists in
					// the tree.
					if (!postAlreadyExists(post, item))
						new PostController(post, item, browser4posts,
										   comp4posts);
				
				// Log the action.
				LogController.log("Blog " + blog.getTitle() +
								  " updated!");
			}
		}
		
		/**
		 * This class implements a task that will update the blog in the
		 * background, freeing the interface for waiting while this work is
		 * being done.
		 * 
		 * Since this class extends thread, to use it you have to create the
		 * task as usual, new UpdateThread(blog) and then call the start()
		 * method of every Thread, that will automatically call the run()
		 * method (implemented here).
		 * 
		 * @author ole
		 */
		private class UpdateThread extends DispatchableTask {
			// Class attributes.
			private Blog blog;
			private List<UpdateCompleteListener> listeners;
			
			/**
			 * Constructor class.
			 * 
			 * @param blog:
			 * 		The blog that will be updated when start() is called.
			 */
			public UpdateThread(Blog blog) {
				this.blog = blog;
				this.listeners = new ArrayList<UpdateCompleteListener>();
				this.setName("Update blog " + blog.getTitle() + " thread.");
			}
			
			/**
			 * This class will call handlers when the update work is done.
			 * 
			 * @param handler
			 * 		The handler to be called when the update is done.
			 */
			public void addUpdateCompleteListener(UpdateCompleteListener handler) {
				listeners.add(handler);
			}
			
			/**
			 * This implements all the work needed to update the blog and send
			 * the signals when the work is done. Will be called when called
			 * the method start().
			 */
			@Override
			public void run() {
				try {
					// Log the action.
					LogController.log("Starting update of blog " +
									  blog.getTitle() + ".");
					
					// Update the blog.
					blog.update(listeners);
				}
				catch (Exception e) {
					RunExceptionBox rebox = new RunExceptionBox(e);
					item.getDisplay().syncExec(rebox);
				}
				
				this.finalize();
			}
		}
		
		/*
		 * Public methods of class UpdateBlogListener.
		 */

		/**
		 * Creates a separate task that will update this blog and when it
		 * completes its works send an event to the interface to notice it.
		 */
		@Override
		public void widgetSelected(SelectionEvent event) {
			UpdateThread uthread;
			
			// Create a task to update the Blog.
			uthread = new UpdateThread(blog);
			
			// Add a handler for when the task has finished.
			// NOTE: Since an update is done by many threads, this instance of
			// UIUpdateCompleteListener is executed many times. This is the
			// reason that this class has a list of events.
			uthread.addUpdateCompleteListener(new UIUpdateCompleteListener());
			
			// Ask the dispatcher to start the update when possible.
			DustDispatcher.dispatchTask(uthread);
		}		
	}
	
	/*
	 * Private methods of class BlogController.
	 */
	
	/**
	 * Method that checks if a Post already exists as a child of node.
	 * This means that node has a PostController which associated Post has
	 * the same title that this Post.
	 * 
	 * @return:
	 * 		True if there is a Post associated to one of the PostControllers
	 * 		of node that has the same title as post, false elsewhere.
	 */
	private boolean postAlreadyExists(Post post, TreeItem node) {
		Post child_post;
		
		// Get every Post of every PostController inside the children of node
		// and checks the equality of the titles.
		for (TreeItem child: node.getItems()) {
			child_post = ((PostController)child.getData()).getPost();
			if (post.getTitle().equals(child_post.getTitle()))
				return true;
		}
		
		return false;
	}
	
	/*
	 * Public methods of class BlogController.
	 */

	/**
	 * Class constructor.
	 * 
	 * @param blog;
	 * 		The blog of this view.
	 * @param tree:
	 * 		The tree where this blog will be attached to.
	 * @param view_comp:
	 * 		The Composite where the BlogView will be shown.
	 * @param browser:
	 * 		The Browser where the Post contents will be shown.
	 * @param postview_comp:
	 * 		The Composite where the PostViews will be shown.
	 */
	public BlogController(Blog blog, Tree tree, DustComposite view_comp,
						  Browser browser, MultiViewComposite postview_comp) {
		MultiViewComposite mvc;
		
		// Create the item in the Tree.
		item = new TreeItem(tree, SWT.NONE);
		item.setText(blog.getTitle());
		item.setData(this);
		
		// The Blog we are maintaining.
		this.blog = blog;
    	
		// Create the BlogView.
    	view = new BlogView(view_comp, blog);
    	view.hide();
    	
    	// If the Composite where we are adding the view is a
    	// MultiViewComposite we have to add this Listener to the view as
    	// specified in the documentation of MultiViewComposite.
    	if (view_comp instanceof MultiViewComposite) {
    		mvc = (MultiViewComposite)view_comp;
    		view.addDisposeListener(mvc.new ChildDisposeListener());
    	}
    	
    	// Create a controller and a view for each post.
    	for (Post post: blog.getPosts())
    		new PostController(post, item, browser, postview_comp);
    	
    	// We need to annotate this because we will need to create new posts
    	// when updates are done (new PostControllers are created then).
    	browser4posts = browser;
    	comp4posts = postview_comp;
	}
	
	/**
	 * Tries to make the Blog associated to this controller to be updated
	 * immediately.
	 */
	public void forceUpdate() {
		UpdateBlogSelectedListener updater;
		SelectionEvent s_event;
		Event event;
		
		// Forcing an update over this Blog its make a bit tricky. We
		// emulate a selection like if the user had made right click over
		// the blog in the tree (displaying the menu) and selecting "update
		// blog".
		event = new Event();
		
		// We need that event.widget isn't null or the SelectionEvent
		// constructor will fail... Why a Button as parent? It is the first
		// class in swt.widgets and i didn't wan't to think too much about
		// this :P.
		event.widget = new Button(getShell(), SWT.NONE);
		s_event = new SelectionEvent(event);
		updater = new UpdateBlogSelectedListener();
		updater.widgetSelected(s_event);
		event.widget.dispose();
	}
	
	/**
	 * Returns the Blog associated of this BlogController.
	 * 
	 * @return:
	 * 		The Blog instance of this BlogController.
	 */
	public Blog getBlog() {
		return blog;
	}
	
	/**
	 * Returns the shell of this BlogView.
	 * 
	 * @return:
	 * 		The shell of this BlogView.
	 */
	public Shell getShell() {
		return item.getDisplay().getShells()[0];
	}
	
	/**
	 * Saves the blog to the path.
	 * 
	 * @param path:
	 * 		The path where the blog should be saved.
	 *   
	 * @throws IOException
	 */
	public void saveBlog(String path) throws IOException {
		blog.save(path);
	}
	
	/**
	 * Shows a menu with the options about a Blog.
	 */
	public void showBlogMenu() {
		Menu menu;
		MenuItem updateblog, addsource, deleteblog, publishblog;
		
		// Create the menu, then fill it with the options.
		menu = new Menu(getShell());
		
		// Option to update the blog.
		updateblog = new MenuItem(menu, SWT.PUSH);
		updateblog.setText("Update this blog");
		updateblog.addSelectionListener(new UpdateBlogSelectedListener());
		
		// Option to publish this blog.
		publishblog = new MenuItem(menu, SWT.PUSH);
		publishblog.setText("Publicate blog");
		publishblog.addSelectionListener(new PublishBlogListener());
		
		// Option to add a new source to a blog.
		addsource = new MenuItem(menu, SWT.PUSH);
		addsource.setText("Add a new HTTP source");
		addsource.addSelectionListener(new AddNewSourceListener());
		
		// Option to add a new public key valid for this blog.
		addsource = new MenuItem(menu, SWT.PUSH);
		addsource.setText("Add a new public key source");
		addsource.addSelectionListener(new AddPublicKeyListener());
		
		// Option to delete my subscription to a blog.
		deleteblog = new MenuItem(menu, SWT.PUSH);
		deleteblog.setText("Delete blog");
		deleteblog.addSelectionListener(new DeleteBlogListener());
		
		// Show the menu.
		menu.setVisible(true);
	}
	
	/**
	 * Tells this BlogController to stop the updatings of its Blog.
	 */
	public void stopUpdate() {
		blog.stopUpdate();
	}
}
